错误处理
PL/SQL运行时的错误可能由设计错误、编码错误、硬件引起失败,还有很多其他的原因。你不能预料到所有可能的错误。但你可以通过异常处理来让你的程序在发生错误后还能继续执行。
PL/SQL运行时的错误处理概述
在PL/SQL中,错误条件称为异常。异常可以是内部定义的(由运行时系统)或用户定义的。内部定义的异常示例有no_data_found(没有找到数据)和zero_divide(除以0)。
您可以在任何PL/SQL块、子程序的声明部分定义自己的异常。例如,您可以定义一个名为“资金不足”的异常来标记透支的银行帐户。必须为用户定义的异常命名。
发生错误时,会引发异常。也就是说,正常的执行停止,控制权转移到PL/SQL块或子程序的异常处理部分。运行时系统隐式(自动)引发内部异常。用户定义的异常必须由RAISE或RAISE_APPLICATION_ERROR抛出。
要处理引发的异常,可以编写称为异常处理程序的单独例程。异常处理程序运行后,当前块停止执行,上层块继续执行下一条语句。如果没有上层块,则返回的上层存储过程,直到返回主机执行环境。
例如除0异常:
DROP TABLE t2; -- t2表用来保存调试信息
CREATE TABLE t2(c1 VARCHAR(200));
CREATE OR REPLACE PROCEDURE pr_divide_zero (sp1 INT, sp2 INT) IS
res float;
BEGIN
res:=sp1/sp2; --这一句发生除0错误
EXCEPTION
WHEN ZERO_DIVIDE THEN --这一句截获刚刚发生的除0错误
INSERT INTO t2 VALUES('zero_divide.'); --记录调试信息到t2
END;
/
CALL pr_divide_zero (1,0); --调用存储过程pr_divide_zero
SELECT * FROM t2; --通过t2查看调试信息
程序执行的流程如下:
- 调用存储过程pr_divide_zero(1,0)
- 在pr_divide_zero内发生除0错误, 此时自动抛出异常zero_divide
- 截获刚刚抛出的zero_divide异常
- 记录调试信息到t2中,然后回退出当前执行块,返回上层块的下一句
- 通过t2查看调试信息
预定义异常
如果PL/SQL程序违反数据库规则或超过系统相关限制,则会自动引发内部异常。PL/SQL将一些常见的错误预定义为异常。例如,如果SELECT-INTO语句不返回行,PL/SQL将引发预定义的异常NO-DATA。
现在支持的预定义异常如下表:
异常名 | 抛出异常的情况 |
---|---|
no_data_found | select into语句一行也没有返回 |
too_many_rows | select into语句返回多于一行数据 |
zero_divide | 尝试除以0 |
dup_val_on_index | 在有唯一索引的列上尝试存入重复值 |
invalid_number | 尝试将字符串转化为数字,但这个字符串不是合法的数字 |
自定义异常
PL/SQL允许您定义自己的异常。与预定义异常不同,必须声明用户定义的异常,然后才能使用RAISE语句或RAISE_APPLICATION_ERROR显式抛出异常。其中RAISE_APPLICATION_ERROR允许您将错误消息与用户定义的异常关联起来。
★声明异常
异常只能在存储过程、PL/SQL块的声明部分声明。通过引入异常的名称和关键字exception来声明异常。
在存储过程的声明部分声明异常:
CREATE TABLE t2(c1 VARCHAR(200));
CREATE OR REPLACE PROCEDURE p_6_3_1_1 (age INT) IS
illegal_age EXCEPTION; -- 声明异常
BEGIN
IF age < 0 THEN
RAISE illegal_age; -- raise an exception that you defined
END IF;
EXCEPTION
WHEN illegal_age THEN
INSERT INTO t2 VALUES('out_of_stock'); -- 插入调试信息
END;
/
在块的声明部分声明异常:
CREATE TABLE t2(c1 VARCHAR(200));
CREATE OR REPLACE PROCEDURE p_6_3_1_2 IS
BEGIN
DECLARE --sub-block begins
subblock_exception EXCEPTION; -- 在块的声明区声明异常
BEGIN
RAISE subblock_exception;
EXCEPTION
WHEN subblock_exception THEN
INSERT INTO t2 VALUES('catch subblock_exception');
END;
end;
/
异常和变量声明类似。但请记住,异常是错误条件,而不是数据项。与变量不同的是,异常不能出现在赋值语句或SQL语句中。和变量相同的是,作用域规则。
★PL/SQL异常的作用域规则
块之间存在两种关系:父子关系和兄弟关系。
多层父子关系构成直系祖先关系。
CREATE OR REPLACE PROCEDURE p_6_3_2_1 IS
BEGIN -- 块1
BEGIN -- 块1.1
BEGIN -- 块1.1.1
END;
BEGIN;
BEGIN -- 块1.2
BEGIN;
END;
块1是存储过程的根块。块1.1,1.2是块1的子块。块1.1和1.2之间是兄弟块。块1.1.1是块1.1的子块。块1是块1.1.1的直系祖先块。
异常作用域规则:
- 不能在同一块中声明两次同名异常
- 一个块中可以声明和他的父块或直系祖先块相同的异常
- 在块中声明的异常被认为是该块的本地异常,而对该块所有子块都是全局异常
- 块内只能引用局部或全局异常,父块不能引用子块中声明的异常
- 如果在子块中重新声明全局异常,则以本地声明为准,此时子块不能引用全局异常
举例说明:
不能在同一块中声明两次异常。如:
DROP PROCEDURE p_6_3_2_2;
CREATE OR REPLACE PROCEDURE p_6_3_2_2 IS
a EXCEPTION; -- 第一次声明异常a
a EXCEPTION; -- 第二次声明异常a, 重复声明
BEGIN
RAISE a;
EXCEPTION
WHEN THEN
NULL;
END;
/
一个块中可以声明和他的父块或直系祖先块相同的异常:
CREATE TABLE t2(c1 VARCHAR(200));
DROP PROCEDURE sp1;
CREATE OR REPLACE PROCEDURE p_6_3_2_3 IS
a EXCEPTION; -- 存储过程的根块声明了异常a
BEGIN
DECLARE
a EXCEPTION; --在子块中也声明了异常a
BEGIN
NULL;
END;
END;
/
CALL sp1();
SELECT * FROM t2;
在块中声明的异常被认为是该块的本地异常,而对该块所有子块都是全局异常。
CREATE OR REPLACE PROCEDURE p_6_3_2_4 IS
a EXCEPTION; -- 存储过程的根块声明了异常a,对于存储过程a是本地异常
BEGIN
-- 这里a是本地异常
BEGIN
-- 这里a 是全局异常
BEGIN
-- 这里a也是全局异常
END;
END;
END;
块内只能引用局部或全局异常,父块不能引用子块中声明的异常。
CREATE OR REPLACE PROCEDURE p_6_3_2_5 IS
a EXCEPTION;
BEGIN -- 块1
DECLARE
b EXCEPTION;
BEGIN -- 块1.1
-- 这里b是本地异常,a是全局异常。
END;
--这里a是本地异常,b不可见。
END;
如果在子块中重新声明全局异常,则以本地声明为准。此时子块不能引用全局异常。
★定义自己的错误消息
RAISE_APPLICATION_ERROR过程允许您从存储的子程序发出用户定义的错误消息。这样,您就可以向应用程序报告错误并避免返回未处理的异常。
要调用RAISE_APPLICATION_ERROR,请使用以下语法:
raise_application_error(error_number, message [, {TRUE | FALSE}]);
其中,error_number是-20000..-20999范围内的负整数,而message是一个最长2048字节的字符串。
应用程序只能从正在执行的存储子程序(或方法)调用raise_application_error。调用时,raise_application_error结束子程序并向应用程序返回用户定义的错误号和消息。错误号和消息可以像其他异常一样被捕获。
示例如下:
CREATE TABLE t2(c1 VARCHAR(200));
CREATE TABLE user_tables(name VARCHAR(50));
INSERT INTO user_tables VALUES('a');
INSERT INTO user_tables VALUES('b');
INSERT INTO user_tables VALUES('c');
CREATE OR REPLACE PROCEDURE p_6_3_3 IS
num_tables NUMBER;
BEGIN
SELECT COUNT(*) INTO num_tables FROM user_tables;
IF num_tables < 10 THEN
RAISE_APPLICATION_ERROR(-20101, 'expecting at least 10 tables'); -- 抛出异常-20101
END IF;
END;
/
DROP PROCEDURE sp2;
CREATE OR REPLACE PROCEDURE sp2 IS
null_salary EXCEPTION;
PRAGMA EXCEPTION_INIT(null_salary, -20101); -- 将异常名null_salary和-20101关联
begin
sp1();
EXCEPTION
WHEN null_salary THEN –- 截获null_salary异常
INSERT INTO t2 VALUES('null_salary happen.');
END;
/
CALL sp2();
SELECT * FROM t2;
此技术允许调用应用程序处理特定异常处理程序中的错误条件。
如何抛出异常
内部异常是由运行时系统隐式抛出的。自定义异常必须显式抛出,包括RAISE语句或调用过程RAISE_APPLICATION_ERROR。有关抛出异常的示例,请参见其它章节。
异常如何传播
当引发异常时,如果PL/SQL在当前块或子程序中找不到该异常的处理程序,则异常会向上传播。也就是说,异常在层层父块中复制自身,直到找到一个处理程序为止。如果最后也找不到处理程序,PL/SQL将向主机环境返回一个未处理的异常错误。
下图说明了基本的传播规则。
子块中抛出的异常A在子块中就被截获并处理了,此时控制权交给了父块。
子块中抛出的异常B在父块中被截获并处理了,此时控制权交给了外部环境。
子块中抛出的异常C没有被截获,因此返回给执行环境一个错误:未处理的异常。
异常可以传播到其作用域之外,也就是说,在声明它的块之外。 例如:
CREATE OR REPLACE PROCEDURE p_6_5 IS
past_due EXCEPTION;--子块中声明的异常
due_date DATE := trunc(SYSDATE) - 1;
todays_date DATE := trunc(SYSDATE);
BEGIN
IF due_date < todays_date THEN
RAISE past_due;--抛出异常past_due
END IF;
------------- 子块结束
EXCEPTION
WHEN OTHERS THEN-- 处理异常
ROLLBACK;
END;
/
由于声明异常past_due的块没有处理异常,因此异常将传播到父块。但是,父块不能引用past_due的名称,因为它声明的范围不再存在。一旦异常名称不再存在,只有OTHERS处理程序才能截获这个异常。
重新抛出异常
有时,需要重新抛出异常,即在当前块处理异常,然后将其传递给父块。例如,您可能希望回滚当前块中的事务,然后在封闭块中记录错误。
若要重新抛出异常,请使用不带异常名称的RAISE语句,该语句仅允许在异常处理程序中使用。例如:
CREATE TABLE t2(c1 VARCHAR(200));
CREATE OR REPLACE PROCEDURE p_6_6 IS
salary_too_high EXCEPTION;
current_salary NUMBER := 20000;
max_salary NUMBER := 10000;
erroneous_salary NUMBER;
BEGIN
BEGIN ---------- sub-block begins
IF current_salary > max_salary THEN
RAISE salary_too_high; -- raise the exception
END IF;
EXCEPTION
WHEN salary_too_high THEN--子块中处理异常
-- first step in handling the error
insert into t2 values('salary_too_high in sub-block.');
RAISE; --重新抛出异常
END; ------------ sub-block ends
EXCEPTION
WHEN salary_too_high THEN--父块中处理异常
-- handle the error more thoroughly
insert into t2 values('salary_too_high in top-block.');
erroneous_salary := current_salary;
current_salary := max_salary;
END;
/
处理抛出的异常
当引发异常时,PL/SQL块或子程序的正常执行将停止,并将控制传输到其异常处理程序,其格式如下:
EXCEPTION
WHEN exception1 THEN -- 处理异常exception1
处理语句序列1
WHEN exception2 THEN -- 处理另一个异常exception2
处理语句序列2
...
WHEN OTHERS THEN -- 处理所有其它异常
处理语句序列3
END;
要捕获引发的异常,请编写异常处理程序。每个处理程序都包含一个WHEN子句,该子句指定一个异常,然后是引发该异常时要执行的一系列语句。这些语句执行完之后,执行控制权不返回抛出异常的位置。换言之,您不能在异常中断的地方继续处理。
可选的OTHERS异常处理程序(始终是块或子程序中的最后一个处理程序)充当所有未明确命名的异常的处理程序。因此,一个块或子程序只能有一个OTHERS处理程序。使用OTHERS处理程序可以保证不会出现未处理的异常。
如果希望两个或多个异常执行相同的语句序列,请在WHEN子句中列出异常名称,并用关键字OR分隔它们,如下所示:
EXCEPTION
WHEN over_limit OR under_limit OR VALUE_ERROR THEN
-- 处理这些异常
如果引发列表中的任何异常,则执行相关的语句序列。关键字OTHERS不能出现在异常名称列表中;它必须自己出现。可以有任意数量的异常处理程序,每个处理程序都可以将异常列表与一系列语句相关联。但是,异常名称只能在PL/SQL块或子程序的异常处理部分出现一次。
PL/SQL变量的通常作用域规则适用,因此您可以在异常处理程序中引用本地和全局变量。但是,当在cursor FOR循环中引发异常时,在调用处理程序之前会隐式关闭游标。因此,显式游标属性的值在处理程序中不可用。
★在声明部分抛出的异常
对于oracle而言,当前块中的处理程序无法捕获引发的异常,因为声明中引发的异常会立即传播到封闭块。
咱们的产品,可以在当前块中截获声明部分抛出的异常。例如:
DROP TABLE t2;
CREATE TABLE t2(c1 VARCHAR(200));
DROP PROCEDURE sp1;
CREATE OR REPLACE PROCEDURE p_6_7_1 (X INT) IS
A EXCEPTION;
begin
DECLARE-- 子块开始
i int := 10/X;-- 声明区发生异常(除以0)
BEGIN
NULL;
EXCEPTION
WHEN OTHERS THEN
INSERT INTO t2 VALUES('咱们的产品可以在这里截获。');
END;-- 子块结束
EXCEPTION
WHEN OTHERS THEN
INSERT INTO t2 VALUES('oracle在这里截获');
end;
/
CALL sp1(0);
SELECT * FROM t2;
★处理在异常处理程序中抛出的异常
当一个异常A在一个异常处理程序中抛出时,同一个异常处理程序无法控制异常A。此时,异常A会传播到父块,从那儿,才能继续截获异常A。比如:
EXCEPTION
WHEN INVALID_NUMBER THEN
INSERT INTO ... -- 可能跑出异常 DUP_VAL_ON_INDEX
WHEN DUP_VAL_ON_INDEX THEN -- 不能截获上一句抛出的DUP_VAAAL_ON_INDEX
END;
★GOTO语句和异常处理
GOTO语句可以从异常处理程序跳转到父块。例如:
CREATE TABLE t2(c1 VARCHAR(200));
CREATE OR REPLACE PROCEDURE p_6_7_3 (X INT) IS
begin
<<label1>>
DECLARE
-- Raises an error:
i int := 10;
BEGIN
i := 10 / x;
INSERT INTO t2 VALUES('now x is not 0');
EXCEPTION
WHEN zero_divide THEN
INSERT INTO t2 VALUES('in sub-block, zero_divide');
x := 10;
GOTO label1;
END;
end;
/
CALL sp1(0);
SELECT * FROM t2;